/*
 * ndmp_thread.c - The NDMP thread which picks up command from the queue and process it
 *
 *      Author: Aboo Valappil
 *      Date: 2/10/2008
 *      Based on: None
 */

#include "ndmp_main.h"

extern struct kmem_cache *ndmp_scsi_task_cache;
struct ndmp_scsi_work_t ndmp_scsi_work;
extern struct ndmp_scsi_sessions sessions;

static inline struct ndmp_scsi_task *next_ndmp_task(struct ndmp_scsi_session *session);
static inline struct ndmp_scsi_task *ndmp_get_task(struct ndmp_scsi_session *session, int seq);
static inline int ndmp_task_state(struct ndmp_scsi_task *task);
static inline void ndmp_set_task_flag(struct ndmp_scsi_task  *task, int flag);
static void ndmp_scan_hba (void *work);

/*
 * kill_ndmp_thread - kills the NDMP thread for the session gracefully
 *
 * This module is usually called either from watchdog or from user ioctl to terminate the NDMP session
 *
 */
int kill_ndmp_thread(struct ndmp_scsi_session *session)
{
//	spin_lock(&session->task_lock);
	 if ( __ndmp_session_state(session, HBA_STATE_THREAD) != _NDMP_STAT_NR ) {
                __ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_NR);
                if (session->ndmp_task) force_sig(SIGKILL, session->ndmp_task);
//		spin_unlock(&session->task_lock);
		return 1;
	}
	else {
//		spin_unlock(&session->task_lock);
		return 0;
	}
}

/*
 * next_ndmp_task - get the next task queued from the queue for processing
 */
static inline struct ndmp_scsi_task *next_ndmp_task(struct ndmp_scsi_session *session)
{

	struct ndmp_scsi_task *task,*tmp;	

	spin_lock(&session->task_lock);
	list_for_each_entry_safe(task, tmp, &session->task_queue, queue) {
		if ( task->flags == NDMP_SCSI_TASK_SUBMITTED ) {
			spin_unlock(&session->task_lock);
			return task;
		}
	}
	spin_unlock(&session->task_lock);
	return NULL;
}

/* ndmp_get_task - gets the task from the queue with a specific sequence number
 *
 * @seq - The sequence number of the task
 * @session - The NDMP session
 */
static inline struct ndmp_scsi_task *ndmp_get_task(struct ndmp_scsi_session *session, int seq)
{

        struct ndmp_scsi_task *task,*tmp;

        spin_lock(&session->task_lock);
        list_for_each_entry_safe(task, tmp, &session->task_queue, queue) {
                if ( task->seq_number == seq ) {
                        spin_unlock(&session->task_lock);
                        return task;
                }
        }
        spin_unlock(&session->task_lock);
        return NULL;
}

/* ndmp_task_state - Gets the status of a task, should the spinlock be used here?
 *
 */
static inline int ndmp_task_state(struct ndmp_scsi_task *task)
{
	return task->flags;
}

/* ndmp_task_state - set the status of a task, should the spinlock be used here?
 *
 * @task - The task structure
 * @flag - The flag to be set
 */
static inline void ndmp_set_task_flag(struct ndmp_scsi_task  *task, int flag)
{
	task->flags=flag;
}

/* ndmp_scsi_complete_task - Competes a task which is already completed on the NDMP server
 * The task is removed from the queue and slab allocation is freed
 */
void ndmp_scsi_complete_task(struct ndmp_scsi_task *task)
{
        struct scsi_cmnd *sc =  task->sc;

        list_del(&task->queue);
        kmem_cache_free(ndmp_scsi_task_cache, task);
        sc->SCp.ptr = NULL;
        if ( task->flags != NDMP_SCSI_TASK_ABORTED ) sc->scsi_done(sc);
}



/*
 * ndmp_thread - The thread which services the SCSI commands from Mid layer.
 * 
 * @param - Pointer to ndmp_scsi_session struct which this thread handles
 *
 *
 * Note: This design only has one thread for both transmit and receive. 
 *       I plan to impliment both transmit and receive threads in the future for concurrent I/O, but we are only dealing with tapes which are sequential access anyway!
 *       The life time of a SCSI host is the life time of this thread, meaning, SCSI host is removed when this thread dies.
 *	 Also removal of SCSI host is done by sending a signal to this thread.
 *
 * The threads blocks at different places for sending and receiving SCSI commands over NDMP. It puts itself to sleep waiting for a task from MID layer.
 * The thread is waken up by the queuecommand mid layer function whenever a new command is ready for transport to NDMP server.
 * The thread is initially created by ndmp_scsi_create_session()
 *
 */
int ndmp_thread (void *param)
{
	
	struct ndmp_scsi_session *session=param;
	struct ndmp_scsi_task *task=NULL;

	printk("ndmphba:%d: ndmp_thread: Starting...\n", session->session_id);
	ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_DONE);

        if ( ndmp_auth(session) ) goto finish_thread; // Connect and authenticate to the NDMP server.

	// The following required to probe the SCSI device using SCSI INQ, is it a Robot or a tape drive? once decided, open it accordingly.
	if ( ndmp_do_scsi_inquiry_and_open_device (session)) goto finish_thread; 

	// Scan the SCSI host to discover devices using schedule work. The scan can not be done inline in this function as it is blocking, 
	// and the thread can not block as it has to serve the SCSI commands generated from scsi_scan_host
        ndmp_scsi_work.session=session;
        NDMP_SCSI_INIT_WORK(&ndmp_scsi_work.work, ndmp_scan_hba);
        schedule_work(&ndmp_scsi_work.work);

	while (1) {

// Wait till a task is availble
		wait_event_interruptible(session->txq, 
				        ( (ndmp_session_state(session,HBA_STATE_CONNECTION) ==  _NDMP_STAT_DONE) 
					&& (task=next_ndmp_task(session)))
					|| ndmp_session_state(session,HBA_STATE_THREAD ) == _NDMP_STAT_NR );

		if (signal_pending(current)) {  
			printk("ndmphba:%d: ndmp_thread: Received signal, exiting ...\n", session->session_id);
                        flush_signals(current);
			if ( test_bit(32,&session->state)) {
				if (ndmp_session_state(session,HBA_STATE_DEVICE) == _NDMP_STAT_DONE) {
					ndmp_close_device(session);
				}
			}
			goto finish_thread;
		}

		if ( ndmp_session_state(session, HBA_STATE_THREAD) == _NDMP_STAT_NR ) {
			printk("ndmphba:%d: ndmp_thread: exiting...\n", session->session_id);
			goto finish_thread;
		}

// Change state to progress for watchdog timer
		ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_IN_PROGRESS);

// The NDMP session only supports LUN 0. If any other luns report, sense data
		if ( task->sc->device->lun != 0 ) {
			spin_lock(&session->task_lock);	
			ndmp_set_sensedata_illegal(task->sc);
			task->sc->result=2;
			scsi_set_resid(task->sc, scsi_bufflen(task->sc));
			ndmp_scsi_complete_task(task);
			__ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_DONE);
			spin_unlock(&session->task_lock);
			continue;
		}

// Set the task state to progress and send the SCSI command over to NDMP server
		ndmp_set_task_flag(task, NDMP_SCSI_TASK_IN_PROGRESS);
		if ( ndmp_exec_scsi_cmd(task) ) {
			printk("ndmphba:%d: ndmp_thread: Execute scsi command failed, exiting...\n", session->session_id);
			goto finish_thread;
		}

// Receives the command results and data
		if ( ndmp_recv_scsi_cmd(task) ) {
			printk("ndmphba:%d: ndmp_thread: Error receiving SCSI command response, exiting...\n", session->session_id);
			goto finish_thread;
		}
// Just a sanity check that the command is not aborted nby any chance while it is progress. Unlikely
		if ( ndmp_get_task(session, session->nh.reply_sequence)) {
			spin_lock(&session->task_lock);
			ndmp_scsi_complete_task(task); // Complete the task
			spin_unlock(&session->task_lock);
		} else printk("ndmphba:%d: ndmp_thread: The current task seems to have been aborted already\n", session->session_id);
// The thread state back to ready state.
		ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_DONE);
	}

finish_thread:

// Control comes here if the thread was aborted by watch dog or IOCTL
	__ndmp_change_session_state(session, HBA_STATE_THREAD, _NDMP_STAT_NR);
	del_timer_sync(&session->watchdog_timer); //Delete the timer, no more watch dog is required as thread exited.
	ndmp_close_socket(session);

	down(&sessions.session_lock);
	ndmp_remove_session(session); // Remove the NDMP session 
	up(&sessions.session_lock);

	return 0; 

}

/*
 * ndmp_scan_hba - Scan the SCSI host to discover SCSI devices served by NDMP session. 
 * Called by the NDMP thread through work queue
 */
static void ndmp_scan_hba (void *work)
{

        struct ndmp_scsi_work_t *s=container_of(work,struct ndmp_scsi_work_t, work);
        struct ndmp_scsi_session *session=s->session;

        scsi_scan_host(session->shost);

}

